Skip to content

前言

记录一下源码中响应式几个忽略的点

get和set中的ob

有两个地方会收集依赖,第一个是在defineReactive时,这个场景是某个属性发生变化时set中dep.notify(),第二个是在observer对象内部,用于手动更新依赖。ob.dep.notify()

js
function defineReactive(obj, key) {
  let dep = new Dep()
  let _value = obj[key]
  let childOb = _value.__ob__

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      if (Dep.target) {
        dep.depend()

        if (childOb) {
          childOb.dep.depend() // 用于$set

          if (Array.isArray(_value)) {
            dependArray(_value) // 用于array的push等方法
          }
        }
      }
    },
  })
}

$set

set绑定在this上面和Vue静态属性上,是一个和实例不太有关系的方法,在实例的开始-beforeCreate阶段执行(stateMixin中执行) this.$set(target, key, value)

  1. 判断target是否是对象或者数组,如果是其他的,打印警告不处理
  2. 如果是数组,判断添加的key是否超长,超长则更新数组length,然后通通按照splice来更新数据(因为数组的splice是魔改的,也能实现响应式)
  3. 对象则需要不能直接对vm.$data 进行操作,也不能对未响应的target进行操作,如果是未响应的数据直接赋值即可
  4. 满足条件的对象,使用defineProperty来添加响应式
  5. 最后调用的target上的dep,通知整个target更新,使用了之前的ob.dep.notify()

array魔改

array上面能够原地修改的方法有sort,reverse, splice 和几个队列栈相关的操作。其实只用关心对数组值新增的值的添加响应式

  1. 初始化数组的响应式和对象差不多,通过对索引依次调用observe,建立响应式逻辑。
  2. 在初始化数组响应式时,如果数组存在上面原型的方法,重写当前方法,将添加的每个值变成响应式。
  3. 通知ob.dep.notify() 更新

watch

watch算是比较简单的,有两种使用方法,在选项中声明watch,二是在原型上面this.$watch 动态声明。watch申明了一个user-watcher, user-watcher会对一个响应式对象进行监听(get),在update中将新值和旧值传递给回调函数。属于最简单的watcher应用。

js
new Watcher(vm, getter, cb)

关键就是这个getter,当执行时,当前全局的Dep.target指向的是当前这个watcher,当getter里面数据变化时,当前这个watcher就会执行, 当watcher初始化时,会立即执行一次来天添加依赖。当依赖变化时会执行watcher.update(), 添加到异步队列,异步队列又会添加到nexttick中依次执行watcher.run,watcher.run 会执行cb(this.getter())

cb(this.getter()),无论是会有两个执行,render-watcher 就是放在this.getter()触发的updateComponent(), 同理computed也是没有CB只有getter

源码中可以发现,还支持sync参数,不讲watcher添加到任务队列,直接更新

computed

computed同理,外号computed-watcher, 和watcher区别是拼接getter函数不一样,getter里面可能会涉及对多个getter对象读取,所以也具有if里面的语句如果第一次未收集不会添加到依赖项中, computed懒实现原理是

nexttick

如果想要获取更新的dom,需要用到该api。nexttick实际就是将代码放置到当前周期最后通过微任务或者宏任务添加。nexttick 函数返回一个promise对象,表示当前这个回调执行完毕

vue
<script>
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'

export default {
  name: 'HomeView',
  data() {
    return {
      msg: 'message',
    }
  },
  components: {
    HelloWorld,
  },
  mounted() {
    this.msg = '123'
    this.$nextTick(() => {
      debugger // 理论上这个debugger时界面应该是白屏
      alert() // 实际上debugger不会阻止渲染线程,还是alert大爷出马
    })
  },
  render(h) {
    const vNode = h('HelloWorld', { props: { msg: this.msg } }, h('div', {}, ''))
    return vNode
  },
}
</script>

异步队列

会将所有set中触发的watcher添加到异步队列中,如果watcher已经存在在队列中,放置到队列最后。异步队列的执行总是在当前周期优先通过nexttick()添加。

$forceUpdate()

同步的执行当前异步队列并清空(不是删除nexttick中执行函数,只是清空),注意同步,意味着不需要再使用nexttick 就能获取更新后dom

上面的理解是错误的, 并不是同步的,forceUpdate只是让当前Vue实例上的所有watcher.upadte(将自身添加到异步队列中,无论对应的依赖是否有更新),话说一个组件中也不会有太多watcher

事件原理

主要是通过对模版的分析转换成vnode,vnode中通过on和nativeOn分别放入componentOptions, 在组件实例化时,如果是原生事件,在转换到dom时添加到dom上面,如果是自定义事件,直接走的Vue.prototype.$on监听(和代码中自定义事件差不多)